Skip to content

Fix TLS certificate validation bypass in non-cURL HTTP path (CWE-295)#276

Open
mynetx wants to merge 1 commit into
jublo:developfrom
mynetx:fix/tls-certificate-validation-cwe-295
Open

Fix TLS certificate validation bypass in non-cURL HTTP path (CWE-295)#276
mynetx wants to merge 1 commit into
jublo:developfrom
mynetx:fix/tls-certificate-validation-cwe-295

Conversation

@mynetx

@mynetx mynetx commented Jul 22, 2026

Copy link
Copy Markdown
Member

Fixes #275

Two bugs in the non-cURL (file_get_contents/stream context) request path

Only reachable when the curl extension is unavailable or setUseCurl(false) is called explicitly.

  1. _getNoCurlInitialization() nested the SSL options under $httpOptions['ssl'], which array_merge_recursive() then placed at $options['http']['ssl']. PHP's https:// stream wrapper only reads SSL context options from the top-level ssl key, so the intended cafile/verify_depth/peer_name settings were silently ignored.
  2. _fetchRemoteFile() explicitly set a top-level 'ssl' => ['verify_peer' => false] for its media-download request, unconditionally disabling certificate validation. Confirmed exploitable with a local TLS server presenting a self-signed certificate for the correct hostname.

Fix

  • Keep the SSL options as a sibling top-level context key instead of nesting them under http, and assign them directly rather than through the recursive merge (a future caller passing its own ssl key could otherwise turn a boolean into an array on collision).
  • Remove the explicit verify_peer => false override in _fetchRemoteFile(), so it inherits the secure defaults.
  • Pass the request hostname into _getNoCurlInitialization() from _fetchRemoteFile() (via parse_url), matching the other two call sites. Without it, peer_name became an explicit empty string instead of being unset, which broke certificate validation for every HTTPS download, not just malicious ones. Caught while verifying the first version of this fix against a real HTTPS URL.
  • Set verify_peer_name explicitly, matching PHP's own 5.6+ default.
  • Bump the composer.json PHP floor from 5.5.0 to 5.6.0, since peer_name/automatic hostname verification require it (5.5 only had the deprecated CN_match option). The README already documents PHP 7.1+ as the real requirement, so this doesn't change what's actually supported in practice.

Verification

The project's PHPUnit suite doesn't run under PHPUnit 13 (legacy test-class naming isn't discovered by the modern loader), so this was verified with a standalone script using reflection to call both affected methods against a local openssl s_server:

  • self-signed certificate, hostname-matching but untrusted: rejected by both _getNoCurlInitialization() and _fetchRemoteFile()
  • real HTTPS request (https://www.google.com/robots.txt): succeeds through both, confirming no regression on legitimate requests

🤖 Generated with Claude Code

Two separate bugs in the non-cURL (file_get_contents/stream context)
request path, only reachable when the curl extension is unavailable
or setUseCurl(false) is called explicitly:

1. _getNoCurlInitialization() nested the SSL options under
   $httpOptions['ssl'], which array_merge_recursive() then placed at
   $options['http']['ssl']. PHP's https:// stream wrapper only reads
   SSL context options from the top-level 'ssl' key, so the intended
   cafile/verify_depth/peer_name settings were silently ignored.

2. _fetchRemoteFile() explicitly set a top-level
   'ssl' => ['verify_peer' => false] for its media-download request,
   unconditionally disabling certificate validation. Confirmed
   exploitable with a local TLS server presenting a self-signed
   certificate for the correct hostname.

Fix:
- Keep the SSL options as a sibling top-level context key instead of
  nesting them under 'http', and assign them directly rather than
  through the recursive merge (a future caller passing its own 'ssl'
  key could otherwise turn a boolean into an array on collision).
- Remove the explicit verify_peer => false override in
  _fetchRemoteFile(), so it inherits the secure defaults.
- Pass the request hostname into _getNoCurlInitialization() from
  _fetchRemoteFile() (via parse_url), matching the other two call
  sites. Without it, peer_name became an explicit empty string
  instead of being unset, which broke certificate validation for
  every HTTPS download, not just malicious ones. Caught while
  verifying the first version of this fix against a real HTTPS URL.
- Set verify_peer_name explicitly, matching PHP's own 5.6+ default.
- Bump the composer.json PHP floor from 5.5.0 to 5.6.0, since
  peer_name/automatic hostname verification require it (5.5 only had
  the deprecated CN_match option). The README already documents
  PHP 7.1+ as the real requirement.

Verification: the project's PHPUnit suite doesn't run under
PHPUnit 13 (legacy test-class naming isn't discovered), so this was
verified with a standalone script using reflection to call both
methods against a local openssl s_server:
- self-signed certificate, hostname-matching but untrusted: rejected
  by both _getNoCurlInitialization() and _fetchRemoteFile()
- real HTTPS request (https://www.google.com/robots.txt): succeeds
  through both, confirming no regression on legitimate requests

Fixes jublo#275
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Security: CWE-295 TLS Certificate Validation Bypass — SSL Context Nesting Bug

1 participant